<?php
namespace plugin\entity_manager\src\query;

/**
 * The MIT License
 *
 * Copyright (c) <year> <copyright holders>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author Damiano Ciarla
 * @package plugin
 * @subpackage entity_manager/class/function
 */
class Find extends AbstractQuery implements InterfaceQuery
{
	/**
	 * Statement sql clausola select
	 *
	 * @var string
	 */
	public $_selectSql;

	/**
	 * Statement sql clausola where
	 *
	 * @var string
	 */
	public $_whereSql;

	/**
	 * Lista dei valori da bindare nelle join
	 *
	 * @var array
	 */
	public $_bindJoinWhereValue = array();

	/**
	 * Lista dei valori da bindare
	 *
	 * @var array
	 */
	public $_bindWhereValue = array();

	/**
	 * Statement sql clausola group by
	 *
	 * @var string
	 */
	public $_groupBySql;

	/**
	 * Statement sql clausola order by
	 *
	 * @var string
	 */
	public $_orderBySql;

	/**
	 * Statement sql clausola limit
	 *
	 * @var string
	 */
	public $_limitSql;

	/**
	 * Variabile che decide se deve essere restituito un solo risultato o una array di risultati
	 *
	 * @var boolean
	 */
	public $_oneResult;

	/**
	 * Oggetto che continene la gestione delle join per la ricerca che si sta effettuando
	 *
	 * @var \plugin\entity_manager\src\join\ManagerFind
	 */
	public $_joinManager;
	
	/**
	 * Variabile che dichiara se si sta inserendo una condizione nella where o meno
	 *
	 * @var boolean
	 */
	private $_orClause = false;
	
	/**
	 * Variabile che dichiara se si sta inserendo una condizione nella where o meno
	 *
	 * @var string
	 */
	private $_lastField = null;

	public function __construct(\plugin\entity_manager\src\AbstractEntity $entity, $oneResult = false)
	{
		$this->_entity = &$entity;
		$this->_oneResult = $oneResult;
	}

	/**
	 * Metodo per scegliere i campi da visualizzare nel risultato finale. Se il metodo non viene chiamato
	 * vengono presi di default tutti i campi della tabella tramite il simbolo '*'
	 *
	 * <code>
	 * EntityManager::count(new User)
	 * ->select("name, username")
	 * ->execute();
	 *
	 * or
	 *
	 * EntityManager::count(new User)
	 * ->select(array("name","username"))
	 * ->execute();
	 * </code>
	 *
	 * @param string|array $fields Lista dei campi da inserire nella select sotto forma di array o stringa con valori separati da virgola
	 *
	 * @return Find
	 */
	public function select($fields)
	{
		$properties = $this->_entity->getProperty();

		if($fields != "*")
		{
			$fields = ((is_string($fields)) ? explode(",", $fields) : $fields);

			if(count($fields) == 0)
			{
				/**
				 * @see \plugin\entity_manager\src\exception\EntityManagerException::NO_FIELD_SET
				 */
				throw new \plugin\entity_manager\src\exception\EntityManagerException(get_class($this->_entity), \plugin\entity_manager\src\exception\EntityManagerException::NO_FIELD_SET);
			}

			foreach($fields as $index => $field)
			{
				$field = trim($field);

				if(!is_string($field))
				{
					/**
					 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_TYPE_IS_NOT_A_STRING
					 */
					throw new \plugin\entity_manager\src\exception\EntityManagerException($field, \plugin\entity_manager\src\exception\EntityManagerException::FIELD_TYPE_IS_NOT_A_STRING);
				}

				if(!key_exists($field, $properties['FIELDS']))
				{
					/**
					 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_SELECT
					 */
					throw new \plugin\entity_manager\src\exception\EntityManagerException(array($field, get_class($this->_entity)), \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_SELECT);
				}

				$fields[$index] = $properties['ENTITY']['TABLE'] . "." . $field;
			}

			$this->_selectSql = $fields;
		}

		$this->_closeIfOpenOrClause();
		return $this;
	}

	/**
	 * Metodo che mette in join una entity con la precedente o con un eventuale entity
	 * specificata nel secondo parametro. Se la relazione non è presente tra le due entità
	 * è possibile specificare su quali campi effettuare la join
	 *
	 * @param \plugin\entity_manager\src\AbstractEntity $entity Istanza della entity su cui fare la join
	 * @param string $previusEntity Nome della entity sui cui agganciare la join ad una entity precedente
	 * @param string $customJoin Relazione tra le due entità specificate in questi parametri
	 *
	 * @return \plugin\entity_manager\src\join\Find
	 */
	public function join(\plugin\entity_manager\src\AbstractEntity $entity, $previusEntity = null, $customJoin = null)
	{
		return $this->_createJoin("INNER JOIN", $entity, $previusEntity, $customJoin);
	}

	/**
	 * Metodo che mette in left join una entity con la precedente o con un eventuale entity
	 * specificata nel secondo parametro. Se la relazione non è presente tra le due entità
	 * è possibile specificare su quali campi effettuare la join
	 *
	 * @param \plugin\entity_manager\src\AbstractEntity $entity Istanza della entity su cui fare la join
	 * @param string $previusEntity Nome della entity sui cui agganciare la join ad una entity precedente
	 * @param string $customJoin Relazione tra le due entità specificate in questi parametri
	 *
	 * @return \plugin\entity_manager\src\join\Find
	 */
	public function leftJoin(\plugin\entity_manager\src\AbstractEntity $entity, $previusEntity = null, $customJoin = null)
	{
		return $this->_createJoin("LEFT JOIN", $entity, $previusEntity, $customJoin);
	}

	/**
	 * Metodo che mette in right join una entity con la precedente o con un eventuale entity
	 * specificata nel secondo parametro. Se la relazione non è presente tra le due entità
	 * è possibile specificare su quali campi effettuare la join
	 *
	 * @param \plugin\entity_manager\src\AbstractEntity $entity Istanza della entity su cui fare la join
	 * @param string $previusEntity Nome della entity sui cui agganciare la join ad una entity precedente
	 * @param string $customJoin Relazione tra le due entità specificate in questi parametri
	 *
	 * @return \plugin\entity_manager\src\join\Find
	 */
	public function rightJoin(\plugin\entity_manager\src\AbstractEntity $entity, $previusEntity = null, $customJoin = null)
	{
		return $this->_createJoin("RIGHT JOIN", $entity, $previusEntity, $customJoin);
	}

	/**
	 * Metodo che istanzia il join manager e crea la prima join specificata nei metodi
	 * "join", "leftJoin", "rightJoin"
	 *
	 * @param string $joinType Tipologia della join
	 * @param \plugin\entity_manager\src\AbstractEntity $entity Istanza della entity su cui fare la join
	 * @param string $previusEntity Nome della entity sui cui agganciare la join ad una entity precedente
	 * @param string $customJoin Relazione tra le due entità specificate in questi parametri
	 *
	 * @return \plugin\entity_manager\src\join\Find
	 */
	private function _createJoin($joinType, \plugin\entity_manager\src\AbstractEntity $entity, $previusEntity = null, $customJoin = null)
	{
		$this->_closeIfOpenOrClause();
		$this->_joinManager = new \plugin\entity_manager\src\join\ManagerFind($this);
		return $this->_joinManager->join($joinType, $entity, $previusEntity, $customJoin);
	}

	/**
	 * Metodo che aggiunge delle custom where da eseguire o savrascrivere a quelle create
	 * dai parametri della entity
	 *
	 * <code>
	 * EntityManager::count(new User)
	 * ->where("name")->METHOD(...)
	 * ->execute();
	 * </code>
	 *
	 * @param string $field Campo della tabella presente nella entity corrente
	 *
	 * @return \plugin\entity_manager\src\clause\where\Find
	 */
	public function where($field)
	{
		$properties = $this->_entity->getProperty();

		if(!key_exists($field, $properties['FIELDS']))
		{
			/**
			 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_WHERE
			 */
			throw new \plugin\entity_manager\src\exception\EntityManagerException(array($field, get_class($this->_entity)), \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_WHERE);
		}

		$this->_closeIfOpenOrClause();
		$this->_lastField = $field;
		return \plugin\entity_manager\src\clause\where\Find::getInstance($this, $field);
	}
	
	/**
	 * Metodo che aggiunge una condizione OR alla WHERE dichiarata precedentemente
	 *
	 * <code>
	 * EntityManager::count(new User)
	 * ->orWhere("name")->METHOD(...)
	 * ->execute();
	 * </code>
	 *
	 * @param string $field Campo della tabella presente nella entity corrente
	 *
	 * @return \plugin\entity_manager\src\clause\where\Find
	 */
	public function orWhere($field)
	{
		if($this->_lastField === null)
			return $this->where($field);
		
		$properties = $this->_entity->getProperty();

		if(!key_exists($field, $properties['FIELDS']))
		{
			/**
			 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_WHERE
			 */
			throw new \plugin\entity_manager\src\exception\EntityManagerException(array($field, get_class($this->_entity)), \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_WHERE);
		}

		if(!$this->_orClause)
			$this->_whereSql = substr_replace($this->_whereSql, " (", strrpos($this->_whereSql, $properties['ENTITY']['TABLE'] . "." . $this->_lastField) - 1, 1);
		
		$this->_orClause = true;

		return \plugin\entity_manager\src\clause\where\Find::getInstance($this, $field, true);
	}

	/**
	 * Metodo che aggiunge delle custom where da eseguire o savrascrivere a quelle create
	 * dai parametri della entity
	 *
	 * <code>
	 * EntityManager::count(new User)
	 * ->where("name")->METHOD(...)
	 * ->execute();
	 * </code>
	 *
	 * @param string $contidion Codice sql della where. Per eseguire il bind dei parametri utilizzare il simbolo '?'
	 * @param array $params Array dei parametri in base al numero di '?' utilizzati nella condizione specificata
	 *
	 * @return Find
	 */
	public function whereCustom($condition, array $params = null, $operator = "AND")
	{
		$this->_whereSql .= " " . $operator . " " . $condition;
		
		if($params !== null)
		{
			foreach($params as $param)
				$this->_bindWhereValue[] = array("type" => \PDO::PARAM_STR, "value" => $param);
		}
		
		return $this;
	}

	/**
	 * Aggiunge un parametro alla clausola order by
	 *
	 * <code>
	 * $User = new User();
	 * $User->name = "John";
	 *
	 * EntityManager::count($User)
	 * ->orderBy("name", "ASC")
	 * ->execute();
	 * </code>
	 *
	 * @param string $field Nome del campo
	 * @param string $value Valore della condizione "asc|desc"
	 *
	 * @return Find
	 */
	public function orderBy($field, $value)
	{
		$properties = $this->_entity->getProperty();

		if(!key_exists($field, $properties['FIELDS']))
		{
			/**
			 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_ORDER_BY
			 */
			throw new \plugin\entity_manager\src\exception\EntityManagerException(array($field, get_class($this->_entity)), \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_ORDER_BY);
		}

		$value = strtoupper($value);

		if($value != "ASC" && $value != "DESC")
		{
			/**
			 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_ORDER_BY
			 */
			throw new \plugin\entity_manager\src\exception\EntityManagerException("", \plugin\entity_manager\src\exception\EntityManagerException::INCORRECT_ORDER_BY_VALUE);
		}

		$this->_closeIfOpenOrClause();
		$this->_orderBySql .= (($this->_orderBySql !== null) ? ", " : "") . $properties['ENTITY']['TABLE'] . "." . $field . " " . $value;

		return $this;
	}

	/**
	 * Metodo per aggiungere uno i più campi da inserire nella group by della query
	 *
	 * <code>
	 * $User = new User();
	 * $User->name = "John";
	 *
	 * EntityManager::count($User)
	 * ->groupBy("name")
	 * ->execute();
	 * </code>
	 *
	 * @return Find
	 */
	public function groupBy($fields)
	{
		$properties = $this->_entity->getProperty();

		$fields = ((is_string($fields)) ? explode(",", $fields) : $fields);

		if(count($fields) == 0)
		{
			/**
			 * @see \plugin\entity_manager\src\exception\EntityManagerException::NO_GROUP_BY_SET
			 */
			throw new \plugin\entity_manager\src\exception\EntityManagerException(get_class($this->_entity), \plugin\entity_manager\src\exception\EntityManagerException::NO_GROUP_BY_SET);
		}

		foreach($fields as $index => $field)
		{
			$field = trim($field);

			if(!key_exists($field, $properties['FIELDS']))
			{
				/**
				 * @see \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_GROUP_BY
				 */
				throw new \plugin\entity_manager\src\exception\EntityManagerException(array($field, get_class($this->_entity)), \plugin\entity_manager\src\exception\EntityManagerException::FIELD_NOT_FOUND_INTO_GROUP_BY);
			}
		}

		$this->_closeIfOpenOrClause();
		$this->_groupBySql = $properties['ENTITY']['TABLE'] . "." . implode(", " . $properties['ENTITY']['TABLE'] . ".", $fields);

		return $this;
	}

	/**
	 * Metodo per aggiungere un limite nel recupero dei risultati
	 *
	 * <code>
	 * EntityManager::count(new User)
	 * ->limit(0, 20)
	 * ->execute();
	 * </code>
	 *
	 * @return Find
	 */
	public function limit($start, $numRows)
	{
		$this->_closeIfOpenOrClause();
		$this->_limitSql = $start . ", " . $numRows;

		return $this;
	}

	/**
	 * Metodo che esegue la query costruita in base ai valori impostati nella entity
	 * o a quelli aggiuntivi settati con i metodi presenti in questa classe
	 *
	 * @param boolean $returnArrayAssoc Parametro che imposta se ricevere il risultato sotto forma di array associativo o di array di entity
	 *
	 * @return array
	 */
	public function execute($returnArrayAssoc = false, $connectionName = null)
	{
		$this->_closeIfOpenOrClause();
		$db = \plugin\entity_manager\src\EntityManager::getConfig($connectionName)->getConnection();
		
		$prepare = $db->prepare($this->_getSqlQuery());

		foreach($this->_bindJoinWhereValue as $param)
			$prepare = $prepare->bindParam("?", $param['value'], $param['type']);

		foreach($this->_bindWhereValue as $param)
			$prepare = $prepare->bindParam("?", $param['value'], $param['type']);

		$result = $prepare->execute();

		if($returnArrayAssoc)
		{
			$result = $result->fetchAll(\PDO::FETCH_ASSOC);
		}
		else
		{
			$result = $result->fetchAll(\PDO::FETCH_CLASS, (($this->_joinManager === null) ? get_class($this->_entity) : "\\plugin\\entity_manager\\src\\collection\\JoinCollectionData"));
		}
		
		if($this->_oneResult)
		{
			if(count($result))
			{
				$result = $result[0];
			}
			else
			{
				$result = null;
			}
		}
		
		return $result;
	}

	/**
	 * Metodo che si occupa di costruire la query in base ai valori impostati nella entity o ai
	 * parametri aggiuntivi settati dall'utilizzo degli altri metodi presenti in questa classe
	 *
	 * @return string
	 */
	private function _getSqlQuery()
	{
		$properties = $this->_entity->getProperty();

		// Verifico se sono presenti delle join impostate. Se non sono state impostate join con
		// tabelle esterne costruisco la query normalmente altrimenti eseguo i controlli per la
		// costruzione della query includendo le relazioni con le altre entity
		if($this->_joinManager === null)
		{
			$sql  =	"SELECT " . (($this->_selectSql === null) ? $properties['ENTITY']['TABLE'] . ".*" : implode(", ", $this->_selectSql)) . " ";

			$sql .=	"FROM " . $properties['ENTITY']['TABLE'] . " ";

			$sql .=	"WHERE 1 " . $this->_whereSql . " ";

			foreach($properties['FIELDS'] as $field => $property)
			{
				if($property['value'] !== null)
				{
					$sql .= " AND " . $properties['ENTITY']['TABLE'] . "." . $field . " = ?";

					$this->_bindWhereValue[] = array("type" => $this->_getBindType($property['properties']['TYPE']), "value" => $property['value']);
				}
			}

			$sql .= (($this->_groupBySql !== null) ? " GROUP BY " . $this->_groupBySql : "");

			$sql .= (($this->_orderBySql !== null) ? " ORDER BY " . $this->_orderBySql : "");

			$sql .= (($this->_oneResult) ? " LIMIT 0, 1" : (($this->_limitSql !== null) ? " LIMIT " . $this->_limitSql : ""));
		}
		else
		{
			$select		= "SELECT " . (($this->_selectSql === null) ? $this->_recreateSelectSqlFromTable($properties, $properties['ENTITY']['TABLE']) : $this->_recreateSelectSqlFromArray($this->_selectSql, $properties['ENTITY']['TABLE']));

			$from		= "FROM " . $properties['ENTITY']['TABLE'] . " ";

			$where		= "WHERE 1 " . $this->_whereSql;

			$groupBy	= (($this->_groupBySql !== null) ? "GROUP BY " . $this->_groupBySql : null);

			$orderBy	= (($this->_orderBySql !== null) ? "ORDER BY " . $this->_orderBySql : null);

			$limit		= (($this->_oneResult) ? " LIMIT 0, 1" : (($this->_limitSql !== null) ? " LIMIT " . $this->_limitSql : null));

			$listJoinTable = $this->_joinManager->getTables();

			// Ciclo la lista degli oggetti che contengono le tabelle da mettere in JOIN
			foreach($listJoinTable as $index => $joinTable)
			{
				// Entity su cui agganciare la join
				$previusTable = (($index == 0) ? $properties : $listJoinTable[$index - 1]->_joinEntity->getProperty());

				// Entity corrente su cui creare la query
				$currentTable = $joinTable->_getEntity()->getProperty();

				// Aggiungo i valori per la select
				$select .= ", " . (($joinTable->_selectSql === null) ? $this->_recreateSelectSqlFromTable($currentTable, $joinTable->_alias) : $this->_recreateSelectSqlFromArray($joinTable->_selectSql, $joinTable->_alias));

				// Aggiungo il codice SQL della join
				$from .= " " . $joinTable->_joinType . " " . $currentTable['ENTITY']['TABLE'] . " AS " . $joinTable->_alias . " ON " . str_replace($currentTable['ENTITY']['TABLE'], $joinTable->_alias, $joinTable->_joinSql);

				// Inserisco eventuali condizioni aggiunti tramite il metodo "where"
				$from .= str_replace($currentTable['ENTITY']['TABLE'], $joinTable->_alias, $joinTable->_whereSql);

				// Ricreo l'array dei valori da bindare mergiando un eventuale array della entity con la lista di quelli già creati
				$this->_bindJoinWhereValue = array_merge($this->_bindJoinWhereValue, $joinTable->_bindWhereValue);

				// Ciclo tutti i campi della tabella che si sta relazionando
				foreach($currentTable['FIELDS'] as $field => $property)
				{
					// Se è presente un valore aggiungo un ulteriore condizione
					if($property['value'] !== null)
					{
						// Aggiungo il valore SQL alle condizioni della join
						$from .= " AND " . $joinTable->_alias . "." . $field . " = ?";

						// Aggiungi il valore da bindare alla lista di quelli già presenti
						$this->_bindJoinWhereValue[] = array("type" => $this->_getBindType($property['properties']['TYPE']), "value" => $property['value']);
					}
				}

				// Verifico se la clausola GROUP BY è stata settata
				if($joinTable->_groupBySql !== null)
				{
					$joinTable->_groupBySql = str_replace($currentTable['ENTITY']['TABLE'], $joinTable->_alias, $joinTable->_groupBySql);
					// Reimposto la clausola "group by" se già settata altrimenti la inizializzo con i nuovi valori
					$groupBy .= (($groupBy == "") ? "GROUP BY " . $joinTable->_groupBySql : ", " . $joinTable->_groupBySql);
				}

				// Verifico se la clausola ORDER BY è stata settata
				if($joinTable->_orderBySql !== null)
				{
					$joinTable->_orderBySql = str_replace($currentTable['ENTITY']['TABLE'], $joinTable->_alias, $joinTable->_orderBySql);
					// Reimposto la clausola "order by" se già settata altrimenti la inizializzo con i nuovi valori
					$orderBy .= (($orderBy == "") ? "ORDER BY " . $joinTable->_orderBySql : ", " . $joinTable->_orderBySql);
				}

				// Se è stato impostato un limite per i risultati lo sostituisco ad un eventuale valore precedente
				$limit = (($this->_oneResult) ? "LIMIT 0, 1" : (($joinTable->_limitSql !== null) ? "LIMIT " . $joinTable->_limitSql : $limit));
			}

			foreach($properties['FIELDS'] as $field => $property)
			{
				if($property['value'] !== null)
				{
					$where .= " AND " . $properties['ENTITY']['TABLE'] . "." . $field . " = ?";

					$this->_bindWhereValue[] = array("type" => $this->_getBindType($property['properties']['TYPE']), "value" => $property['value']);
				}
			}

			$sql  = trim($select)	. " " .
					trim($from)		. " " .
					trim($where)	. " " .
					trim($groupBy)	. " " .
					trim($orderBy)	. " " .
					trim($limit);
		}
		
		return $sql;
	}

	/**
	 * Metodo che ricrea il codice SQL per la clausola select partendo dall'array
	 * dei valori settati nel metodo "select"
	 *
	 * @param string $listSelectFields Lista dei campi selezionati
	 * @param string $alias Alias della tabella
	 *
	 * @return string
	 */
	private function _recreateSelectSqlFromArray($listSelectFields, $alias)
	{
		foreach($listSelectFields as $index => $field)
			$listSelectFields[$index] = $alias . "." . substr($field, strpos($field, ".") + 1) . " AS " . $alias . "_" . substr($field, strpos($field, ".") + 1);

		return implode(", ", $listSelectFields);
	}

	/**
	 * Metodo che ricrea il codice SQL per la clausola select partendo dalla descrizione
	 * della tabella e sostituendo il nome della tabella con l'alias passato nel parametro del metodo
	 *
	 * @param string $listSelectFields Lista dei campi selezionati
	 * @param string $alias Alias della tabella
	 *
	 * @return string
	 */
	private function _recreateSelectSqlFromTable($tableProperties, $alias)
	{
		$listSelectFields = array();

		foreach($tableProperties['FIELDS'] as $field => $info)
			$listSelectFields[] = $alias . "." . $field . " AS " . $alias . "_" . $field;

		return implode(", ", $listSelectFields);
	}
	
	/**
	 * Metodo che verifica se è stata aperta la condizione OR all'interno della WHERE
	 */
	private function _closeIfOpenOrClause()
	{
		if($this->_orClause)
			$this->_whereSql .= ")";
		
		$this->_orClause = false;
	}
}
?>
